<!--
 * @Author: lmh
 * @Date: 2021-08-09 22:11:19
 * @LastEditTime: 2021-09-12 16:59:31
 * @LastEditors: lmh
 * @Description: 发布订阅者模式
-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="root">
        <div class="tt1" title="sss0">测试1</div>
        <div title="sss1">{{ name }}</div>
        <div title="sss2">{{ age }}</div>
        <div title="sss3">{{ gender }}</div>
        <ul>
            <li>排1</li>
            <li>排5</li>
            <li>排3</li>
            <li>排4</li>
            <li>排2</li>
        </ul>
    </div>
    <script>
        // 虚拟DOM
        class VNode{
            constructor(tag, data, value, type) {
                this.tag = tag?.toLowerCase()
                this.data = data
                this.value = value
                this.type = type
                this.children = []
            }
            appendChild(vnode) {
                this.children.push(vnode)
            }
        }

        // 由HTML DOM 生成 VNode 
        function getVnode(node) {
            let nodeType = node.nodeType
            let _vnode = null
            if (nodeType == 1) { // 元素
                let nodeName = node.nodeName
                let attrs = node.attributes
                let _attrObj = {}
                for(let i = 0; i < attrs.length; i++) {
                    _attrObj[attrs[i].nodeName] = attrs[i].nodeValue 
                }
                _vnode = new VNode(nodeName, _attrObj, undefined, nodeType)

                let childNodes = node.childNodes
                for(let i = 0; i < childNodes.length; i++) {
                    _vnode.appendChild(getVnode(childNodes[i]))
                } 
            } else if (nodeType == 3) {
                _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
            }

            return _vnode
        }

        // VNode 到真正的 DOM
        function parseVNode(vnode) {
            // 创建 真实的 DOM
            let type = vnode.type
            let _node = null
            if (type === 3) {
                return document.createTextNode(vnode.value) // 创建文本节点
            } else if (type === 1) {
                _node = document.createElement(vnode.tag)

                // 属性
                let data = vnode.data // 现在这个 data 是键值对
                Object.keys(data).forEach(key => {
                    let attrName = key
                    let attrValue = data[key]
                    _node.setAttribute(attrName, attrValue)
                })

                // 子元素
                let children = vnode.children
                children.forEach(subvnode => {
                    _node.appendChild(parseVNode(subvnode)) // 递归转换子元素 （虚拟 DOM）
                })

                return _node
            }
        }

        let rkuohao = /\{\{(.+?)\}\}/g; // 匹配花括号内部的东西
        // 根据路径访问对象成员
        function getValueByPath(obj, path) {
            let paths = path.split(',')
            let res = obj
            let prop
            while (prop = paths.shift()) {
                res = res[prop]
            }
            return res
        }
        // 将带有坑的vnode 与数据data结合 得到填充数据的vnode 模拟AST -> vnode行为
        function combine(vnode, data) {
            let _type = vnode.type
            let _data = vnode.data
            let _value =  vnode.value
            let _tag = vnode.tag
            let _children = vnode.children

            let _vnode = null

            if (_type === 3) {
                // 对文本处理
                _value = _value.replace(rkuohao, function(_, g) {
                    return getValueByPath(data, g.trim())
                })
                _vnode = new VNode(_tag, _data, _value, _type)
            } else if (_type === 1) {
                _vnode = new VNode(_tag, _data, _value, _type);
                _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data)));
            }

            return _vnode
        }

        function JGVue(options) {
            this._data = options.data 
            let elm = document.querySelector(options.el) // vue中是字符串，这里是DOM
            this._template = elm
            this._parent = elm.parentNode

            reactify(this._data, this)

            this.mounted() // 挂载
        }

        JGVue.prototype.mounted = function() {

            this.render = this.createRenderFn() // vue中可以自定义render，所以放在这个位置 带有缓存功能

            this.mountComponent()
        }

        // 执行mountComponent()函数
        JGVue.prototype.mountComponent = function() {
            let mount = () => {
                this.updated(this.render())
            }

            mount.call(this) // 本质应该交给watcher，但此刻先调就好了
        }

        /**
         * 在真正的 vue 中使用 二次提交 的设计结构
         * 1、在页面中的 DOM 和 虚拟DOM 是一一对应的关系
         * 2、先在 AST 和 数据生成 VNode （新 render）
         * 3、将旧的vnode 和新的 vnode diff （更新 update）
        */

        // 生成新的虚拟DOM（实则生成抽象语法树）
        // JGVue.prototype.render() = function() {}
        // 这里生成 render 函数，目的是缓存 抽象语法树 （我们使用虚拟DOM模拟）
        JGVue.prototype.createRenderFn = function() {
            let ast = getVnode(this._template) // AST缓存
            // Vue: AST + data = vnode
            // 将带坑的Vnode+data => 含有数据的vnode
            return function render() {
                let _tmp = combine(ast, this._data)
                return _tmp
            }
        }

        // 将虚拟DOM渲染到页面上：diff算法 （比较新旧虚拟DOM，然后进行更新）
        JGVue.prototype.updated = function(vnode) {
            // 简化， 直接生成HTML DOM replaceChild 到页面中
            // 父元素替换
            let realDOM = parseVNode(vnode)
            
            this._parent.replaceChild(realDOM, document.querySelector('#root'))
            // 每次会将页面的 DOM 全部替换
        }

        // 响应式化部分
        let ARRY_METHOD = [
            'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'
        ]
        let array_methods = Object.create(Array.prototype)

        ARRY_METHOD.forEach(method => {
            array_methods[method] = function() {
                // 调用原来的方法
                console.log('调用的是拦截的 ' + method + ' 方法')

                // 将数据进行响应式化
                for(let i = 0; i < arguments.length; i++) {
                    reactify(arguments[i])
                }
                let res = Array.prototype[method].apply(this, arguments)
                return res
            }
        })

        // 简化后版本
        function defineReactive(target, key, value, enumerable) {
            // 函数内部是一个局部作用域， 这个value就只在函数内部使用的变量（闭包）
            // 折中处理后，this就是vue实例
            that = this
            if (typeof value === 'object' && value != null && !Array.isArray(value)) {
                // 非数组引用类型
                reactify(value) // 递归
            }

            Object.defineProperty(target, key, {
                configurable: true,
                enumerable: !!enumerable,
                get () {
                    console.log(`读取 o 的 ${key} 属性`)
                    return value
                },
                set (newVal) {
                    console.log(`设置 o 的 ${key} 属性为：${newVal}`)
                    value = reactify(newVal)

                    // 模板刷新（这现在是假的，只是演示）
                    // 如何获取vue实例 watcher 就不会存在这个问题
                    that.mountComponent()
                }
            })
        }

        // 将对象进行响应式化
        function reactify(o, vm) {
            let keys = Object.keys(o)

            for(let i = 0; i < keys.length; i++) {
                let key = keys[i] // 属性名
                let value = o[key]
                if ( Array.isArray(value) ) {
                    // 数组
                    value.__proto__ = array_methods // 数组就响应式了
                    for(let j = 0; j < value.length; j++) {
                        reactify(value[j], vm) // 递归
                    }
                } else {
                    // 对象或者值类型
                    defineReactive.call(vm, o, key, value, true)
                }

                // 只需要在这里添加代理即可 （但此地存在递归）
                // 如果在这里将属性映射到vue实例上，那么就表示Vue实例可以使用属性 key
            }
        }
    
        let app = new JGVue({
            el: '#root',
            data: {
                name: '张三',
                age: 12,
                gender:'male',
                datas: [
                    { info: '好难啊' },
                    { info: '真难啊' },
                    { info: '真的好难啊' },
                    { info: '联系一下' },
                    { info: '练习两下' },
                ]
            }
        })

        // 修改数据时模板要刷新
    </script>
</body>
</html>